import os
import shutil
import uuid
from pathlib import Path

import networkx as nx
from git import Repo, GitCommandError
from unidiff.patch import PatchSet

from parser import Parser
from utils import get_logger
from .mergehandler import GitMergeHandler

_gitignore = \
    """# 操作系统生成的文件
    ## macOS
    .DS_Store
    .AppleDouble
    .LSOverride
    ._*
    
    ## Windows
    Thumbs.db
    Thumbs.db:encryptable
    ehthumbs.db
    ehthumbs_vista.db
    [Dd]esktop.ini
    *.lnk
    
    ## Linux
    *~
    .fuse_hidden*
    .directory
    .Trash-*
    .nfs*
    
    # IDE 和编辑器
    ## Visual Studio Code
    .vscode/
    *.code-workspace
    
    ## JetBrains IDEs
    .idea/
    *.iml
    *.iws
    .idea_modules/
    
    ## Eclipse
    .metadata
    bin/
    tmp/
    *.tmp
    *.bak
    *.swp
    .settings/
    .project
    .classpath
    """


class GitProjectMeta(type):
    def __call__(cls, *args, **kwargs):
        raise RuntimeError("请使用 git_init() 方法创建实例")


class GitProject(Parser, metaclass=GitProjectMeta):
    @classmethod
    def git_init(cls, source_path: str | Path, temp_path: str | Path = './temp'):
        """
        初始化项目管理器，会删除.git
        :param source_path: 源项目路径
        :param temp_path: 临时项目的根路径
        """
        instance = cls.__new__(cls)
        instance.__init__(source_path, temp_path)

        git_path = instance.source_path / '.git'
        if git_path.exists():
            shutil.rmtree(git_path)

        # 如果项目路径是空的，会添加一个.gitignore
        if not any(instance.source_path.iterdir()):
            with open(instance.source_path / '.gitignore', 'w', encoding='utf-8') as f:
                f.write(_gitignore)

        # 初始化git仓库
        repo = Repo.init(source_path)

        instance.repo = repo
        instance.repo.git.add('.')
        instance.repo.git.commit('-m', "Initial commit")

        instance.branch_info = {instance.current_branch: ''}

        instance.add_commit_node()
        instance._subgraph_node_head = instance.current_commit['hash']

        return instance

    def __init__(self, source_path: str | Path, temp_path: str | Path = './temp'):
        super().__init__()
        self.source_path = Path(source_path).resolve()
        self.source_path.mkdir(parents=True, exist_ok=True)

        self.temp_base = Path(temp_path).resolve()
        self.temp_base.mkdir(parents=True, exist_ok=True)

        self.repo = None
        self.branch_info = None  # 用于存储分支信息
        self.parent_branch = {}

    @property
    def current_branch(self):
        return self.repo.active_branch.name

    @property
    def current_commit(self):
        latest_commit = self.repo.head.commit
        return {
            'hash': latest_commit.hexsha,
            'message': latest_commit.message,
        }

    def add_commit_node(self):
        latest_commit = self.current_commit
        self.graph.add_node(latest_commit['hash'],
                            title=latest_commit['message'],
                            label=latest_commit['hash'][:7],
                            type=self.current_branch,
                            shape='box',
                            font={'size': 14})

    def create_branch(self, new_branch: str, temp_path: str | Path = None) -> 'GitProject':
        """
        克隆项目到新目录并根据当前分支创建新分支
        :param new_branch: 新分支名称
        :param temp_path: 指定临时目录，默认根据uuid创建
        :return: 新Git项目
        """
        # 创建唯一的临时目录
        temp_path = None if temp_path is None else Path(temp_path)
        if temp_path is None:
            temp_dir = self.temp_base / f"proj_{uuid.uuid4().hex[:16]}"
        elif temp_path.is_absolute():
            temp_dir = temp_path
        else:
            temp_dir = self.temp_base / temp_path
        temp_dir.mkdir(parents=True, exist_ok=True)

        # 克隆仓库
        repo = Repo.clone_from(str(self.source_path), str(temp_dir))

        # 切换到基础分支
        repo.git.checkout(self.current_branch)

        # 创建并切换到新分支
        repo.git.checkout('-b', new_branch)

        instance = GitProject.__new__(GitProject)
        instance.__init__(temp_dir, self.temp_base)
        new_project = instance
        new_project.repo = repo

        new_project.branch_info = {new_branch: self.current_branch}
        new_project.parent_branch = self.branch_info.copy()

        new_project.graph = self.graph.copy()
        new_project._subgraph_node_head = self.current_commit['hash']
        return new_project

    def checkout(self, branch_name: str):
        """
        切换分支
        :param branch_name: 分支名
        """
        self.repo.git.checkout(branch_name)

    def commit(self, files, message: str):
        """
        提交更改
        :param files: 要提交的文件列表
        :param message: 提交信息
        """
        last_commit = self.current_commit['hash']

        self.repo.index.add(files)
        self.repo.index.commit(message)

        self.add_commit_node()
        self.graph.add_edge(self.current_commit['hash'], last_commit, arrows='from')

    def merge(self, project: 'GitProject'):
        """
        将其他的分支合并到当前仓库
        :param project: 要合并的仓库，合并当前分支
        """
        current_branch = project.current_branch

        for branch_name in project.branch_info.keys():
            # push 这里push虽然可以保存更多分支信息，但是会太多了
            project.repo.git.push('origin', branch_name)
            # 更新信息
            if branch_name not in self.branch_info:
                self.branch_info[branch_name] = project.branch_info[branch_name]

        self_last_commit = self.current_commit['hash']
        project_last_commit = project.current_commit['hash']

        # 在源仓库中合并分支
        try:
            # project.repo.git.push('origin', current_branch)
            self.repo.git.merge(current_branch)
        except GitCommandError as e:
            log_path = os.environ['LOG_PATH'] if 'LOG_PATH' in os.environ else './log'
            log_path = Path(log_path).resolve()
            log_path.mkdir(parents=True, exist_ok=True)
            log_path = log_path / 'git_merge.log'
            logger = get_logger('git_merge', log_path)
            logger.info(f'{self.source_path / self.current_branch} merge '
                        f'{project.source_path / current_branch} conflict')
            GitMergeHandler(self.source_path, self.current_branch, current_branch).handle_merge()

        current_commit = self.current_commit['hash']

        new_graph = nx.compose(self.graph, project.graph)

        self.graph = new_graph
        if current_commit != project_last_commit:
            self.add_commit_node()
            self.graph.add_edge(current_commit, self_last_commit, arrows='from')
            self.graph.add_edge(current_commit, project_last_commit, arrows='from')

    def get_current_commit_add_code(self, file_path):
        commits = list(self.repo.iter_commits(self.current_branch, max_count=2))
        new_commit, old_commit = commits
        diff = self.repo.git.diff(old_commit, new_commit, file_path)

        file_path = file_path.relative_to(self.source_path)
        patch = PatchSet(diff)
        target_patch = None
        for patched_file in patch:
            if Path(patched_file.path) == file_path:
                target_patch = patched_file
                break
        if target_patch is None:
            raise FileNotFoundError(f"找不到{self.source_path / file_path}")
        code = ''
        hunks = []
        for hunk in target_patch:
            block = []
            for line in hunk:
                if line.is_added:
                    block.append(line.value)
                elif line.is_removed:
                    pass
            hunks.append(''.join(block))
        for block in hunks:
            code += f"\n{block.strip()}\n"
        return code

    def _custom_visual_network(self, net):
        net.options.edges.smooth.enabled = False

        net.barnes_hut(
            gravity=-8000,  # 负值会增加节点间距
            central_gravity=0.05,  # 控制向中心的引力
            spring_length=90,  # 弹簧自然长度
            spring_strength=0.3,  # 弹簧强度
            damping=0.09  # 阻尼系数
        )
        return net

    def remove(self):
        self.repo.close()
        shutil.rmtree(self.source_path)

    def __del__(self):
        self.repo.close()